﻿// -----------------------------------------------------------------------
// <copyright file="ForEachExpression.cs" company="none">
// Copyright (c) 2012 M. Alberti, xanatos(at)live.it
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
// </copyright>
// -----------------------------------------------------------------------

namespace FluentStatement
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Utilities;

    /// <summary>
    /// 
    /// </summary>
    public class ForEachExpression : Expression
    {
        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo IEnumerableGetEnumerator = typeof(IEnumerable).GetMethod("GetEnumerator");

        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo IEnumeratorMoveNext = typeof(IEnumerator).GetMethod("MoveNext");

        /// <summary>
        /// 
        /// </summary>
        protected static readonly PropertyInfo IEnumeratorCurrent = typeof(IEnumerator).GetProperty("Current");

        /// <summary>
        /// 
        /// </summary>
        protected static readonly MethodInfo IDisposableDispose = typeof(IDisposable).GetMethod("Dispose");

        /// <summary>
        /// 
        /// </summary>
        protected readonly Expression Enumerable2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly LambdaExpression Body2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly LabelTarget BreakLabel2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly LabelTarget ContinueLabel2;

        /// <summary>
        /// 
        /// </summary>
        protected readonly bool IsArray;

        /// <summary>
        /// 
        /// </summary>
        protected readonly MethodInfo GetEnumerator;

        /// <summary>
        /// 
        /// </summary>
        protected readonly MethodInfo MoveNext;

        /// <summary>
        /// 
        /// </summary>
        protected readonly PropertyInfo Current;

        /// <summary>
        /// 
        /// </summary>
        protected readonly Expression Expression2;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="enumerable"></param>
        /// <param name="body"></param>
        /// <param name="break"></param>
        /// <param name="continue"></param>
        public ForEachExpression(Expression enumerable, LambdaExpression body, LabelTarget @break, LabelTarget @continue)
        {
            if (enumerable == null)
            {
                throw new ArgumentNullException("enumerable");
            }

            if (body == null)
            {
                throw new ArgumentNullException("body");
            }

            if (body.Parameters.Count != 1)
            {
                throw new ArgumentException("body");
            }

            if (@break != null && @break.Type != typeof(void) && !body.Body.Type.IsSimplyAssignableFrom(@break.Type))
            {
                throw new ArgumentException("@break");
            }

            this.Enumerable2 = enumerable;
            this.Body2 = body;
            this.BreakLabel2 = @break ?? Expression.Label(body.Body.Type);
            this.ContinueLabel2 = @continue ?? Expression.Label();

            this.IsArray = FindIEnumerableIEnumeratorMethods(enumerable.Type, out this.GetEnumerator, out this.MoveNext, out this.Current);

            if (this.IsArray)
            {
                if (!body.Parameters[0].Type.IsSimplyAssignableFrom(enumerable.Type.GetElementType()))
                {
                    throw new NotSupportedException("Different types");
                }
            }
            else
            {
                if (!body.Parameters[0].Type.IsSimplyAssignableFrom(this.Current.PropertyType))
                {
                    this.GetEnumerator = null;
                    this.MoveNext = null;
                    this.Current = null;
                    throw new NotSupportedException("Different types");
                }
            }

            this.Expression2 = this.InternalReduce();
        }

        /// <summary>
        /// 
        /// </summary>
        public Expression Enumerable
        {
            get
            {
                return this.Enumerable2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public LambdaExpression Body
        {
            get
            {
                return this.Body2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public LabelTarget BreakLabel
        {
            get
            {
                return this.BreakLabel2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public LabelTarget ContinueLabel
        {
            get
            {
                return this.ContinueLabel2;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override bool CanReduce
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override ExpressionType NodeType
        {
            get
            {
                return ExpressionType.Extension;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public override Type Type
        {
            get
            {
                return this.BreakLabel.Type;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override Expression Reduce()
        {
            return this.Expression2;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="collectionType"></param>
        /// <param name="getEnumerator"></param>
        /// <param name="moveNext"></param>
        /// <param name="current"></param>
        /// <returns>True if collectionType is a T[].</returns>
        protected static bool FindIEnumerableIEnumeratorMethods(Type collectionType, out MethodInfo getEnumerator, out MethodInfo moveNext, out PropertyInfo current)
        {
            if (collectionType.IsArray)
            {
                // Standard array T[], not a T[*] or a T[,...]
                if (collectionType == collectionType.GetElementType().MakeArrayType())
                {
                    getEnumerator = null;
                    moveNext = null;
                    current = null;
                    return true;
                }
            }

            if (collectionType.IsNullable())
            {
                collectionType = collectionType.GetGenericArguments()[0];
            }

            MethodInfo[] methods = collectionType.GetVisibleMethods().Where(p => p.Name == "GetEnumerator" && !p.IsStatic && !p.IsGenericMethod && p.GetParameters().Length == 0).ToArray();

            if (methods.Length == 1)
            {
                getEnumerator = methods[0];

                if (!methods[0].ReturnType.IsClass && !getEnumerator.ReturnType.IsValueType && !getEnumerator.ReturnType.IsInterface)
                {
                    getEnumerator = null;
                    moveNext = null;
                    current = null;
                    throw new NotSupportedException("Unsupported return type for GetEnumerator()");
                }

                Type enumeratorType = getEnumerator.ReturnType;

                PropertyInfo[] properties = (from p in enumeratorType.GetVisibleProperties()
                                             where p.Name == "Current"
                                             let get = p.GetGetMethod(false)
                                             where get != null && !get.IsStatic && p.GetIndexParameters().Length == 0
                                             select p).ToArray();

                if (properties.Length != 1)
                {
                    getEnumerator = null;
                    moveNext = null;
                    current = null;
                    throw new NotSupportedException("Unsupported/multiple/missing Current property");
                }

                current = properties[0];

                methods = enumeratorType.GetVisibleMethods().Where(p => p.Name == "MoveNext" && !p.IsStatic && !p.IsGenericMethod && p.ReturnType == typeof(bool) && p.GetParameters().Length == 0).ToArray();

                if (methods.Length != 1)
                {
                    getEnumerator = null;
                    moveNext = null;
                    current = null;
                    throw new NotSupportedException("Unsupported/multiple/missing MoveNext method");
                }

                moveNext = methods[0];

                return false;
            }

            Type[] interfaces = collectionType.GetInterfaces(typeof(IEnumerable<>));

            if (interfaces.Length > 1)
            {
                throw new NotSupportedException("Multiple IEnumerable<T> interfaces");
            }

            if (interfaces.Length == 1)
            {
                getEnumerator = interfaces[0].GetMethod("GetEnumerator");

                Type enumeratorType = getEnumerator.ReturnType;

                current = enumeratorType.GetProperty("Current");
                moveNext = IEnumeratorMoveNext;

                return false;
            }

            // No IEnumerable<T>
            if (typeof(IEnumerable).IsAssignableFrom(collectionType))
            {
                getEnumerator = IEnumerableGetEnumerator;
                current = IEnumeratorCurrent;
                moveNext = IEnumeratorMoveNext;

                return false;
            }

            getEnumerator = null;
            moveNext = null;
            current = null;

            throw new NotSupportedException("No GetEnumerator method found");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected Expression InternalReduce()
        {
            Expression expression;

            if (this.IsArray)
            {
                expression = this.ForEachTArrayStatement();
            }
            else
            {
                expression = this.ForEachTEnumerableStatement();
            }

            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        protected Expression ForEachTEnumerableStatement()
        {
            Type enumeratorType = this.GetEnumerator.ReturnType;

            // Note that C# doesn't seem to support nullable value types as enumerators.
            ParameterExpression enumerator = Expression.Variable(this.GetEnumerator.ReturnType);
            ParameterExpression current = this.Body.Parameters[0];
            ParameterExpression value = null;

            var parameters = new List<ParameterExpression>();

            parameters.Add(enumerator);
            parameters.Add(current);

            if (this.Type != typeof(void))
            {
                value = Expression.Variable(this.Type);
                parameters.Add(value);
            }

            var block = new List<Expression>();

            block.Add(Expression.IfThen(Expression.Not(Expression.Call(enumerator, this.MoveNext)), Expression.Break(this.BreakLabel, value)));
            block.Add(Expression.Assign(current, Expression.Property(enumerator, this.Current)));

            if (value != null)
            {
                block.Add(Expression.Assign(value, this.Body.Body));
            }
            else
            {
                block.Add(this.Body.Body);
            }

            List<ParameterExpression> finallyBlockParameters = null;
            List<Expression> finallyBlock = null;

            bool isDisposable = typeof(IDisposable).IsAssignableFrom(enumeratorType);
            bool needDispose = isDisposable || !enumeratorType.IsSealed;

            if (needDispose)
            {
                finallyBlockParameters = new List<ParameterExpression>();
                finallyBlock = new List<Expression>();

                if (enumeratorType.IsValueType)
                {
                    finallyBlock.Add(Expression.Call(enumerator, IDisposableDispose));
                }
                else if (isDisposable)
                {
                    finallyBlock.Add(Expression.IfThen(Expression.ReferenceNotEqual(enumerator, Expression.Constant(null)), Expression.Call(enumerator, IDisposableDispose)));
                }
                else
                {
                    ParameterExpression disposable = Expression.Variable(typeof(IDisposable));

                    finallyBlockParameters.Add(disposable);
                    finallyBlock.Add(Expression.Assign(disposable, Expression.TypeAs(enumerator, typeof(IDisposable))));
                    finallyBlock.Add(Expression.IfThen(Expression.ReferenceNotEqual(disposable, Expression.Constant(null)), Expression.Call(disposable, IDisposableDispose)));
                }
            }

            Expression loopBlock = Expression.Loop(Expression.Block(block), this.BreakLabel, this.ContinueLabel);

            Expression expression = Expression.Block(
                parameters,
                new Expression[] 
                {
                    Expression.Block(new Expression[] 
                    {
                        // If enumerable is a nullable value type, we need to use the Value property.
                        Expression.Assign(
                            enumerator, 
                            Expression.Call(
                                this.Enumerable.Type.IsNullable() ? 
                                    Expression.Property(this.Enumerable, this.Enumerable.Type.GetProperty("Value")) : 
                                    this.Enumerable, 
                                    this.GetEnumerator)),

                        needDispose ?
                            Expression.TryFinally(
                                loopBlock, 
                                Expression.Block(finallyBlockParameters, finallyBlock)) :
                            loopBlock, 
                    })
                });

            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        protected Expression ForEachTArrayStatement()
        {
            // For single dimensional arrays we use an optimized code that does a for cycle
            ParameterExpression enumerable = Expression.Variable(this.Enumerable.Type);
            ParameterExpression index = Expression.Variable(typeof(int));
            ParameterExpression current = this.Body.Parameters[0];

            Expression expression = Expression.Block(
                new[] { enumerable, index, current },
                new Expression[] 
                {
                    Expression.Assign(enumerable, this.Enumerable),
                    ExpressionEx.For(
                        Expression.PostIncrementAssign(index), 
                        Expression.LessThan(index, Expression.ArrayLength(enumerable)), 
                        Expression.Block(
                            Expression.Assign(current, Expression.ArrayAccess(enumerable, index)), 
                            this.Body.Body), 
                        this.BreakLabel, 
                        this.ContinueLabel)
                });

            return expression;
        }
    }
}